/*
 * SonarQube
 * Copyright (C) 2009-2023 SonarSource SA
 * mailto:info AT sonarsource DOT com
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */
import { screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { keyBy, times } from 'lodash';
import React from 'react';
import { act } from 'react-dom/test-utils';
import { Route } from 'react-router-dom';
import selectEvent from 'react-select-event';
import { byLabelText, byRole, byText } from 'testing-library-selector';
import { ProjectActivityServiceMock } from '../../../../api/mocks/ProjectActivityServiceMock';
import { TimeMachineServiceMock } from '../../../../api/mocks/TimeMachineServiceMock';
import { parseDate } from '../../../../helpers/dates';
import { mockComponent } from '../../../../helpers/mocks/component';
import {
  mockAnalysis,
  mockAnalysisEvent,
  mockHistoryItem,
  mockMeasureHistory,
} from '../../../../helpers/mocks/project-activity';
import { get } from '../../../../helpers/storage';
import { mockMetric } from '../../../../helpers/testMocks';
import {
  dateInputEvent,
  renderAppWithComponentContext,
} from '../../../../helpers/testReactTestingUtils';
import { ComponentQualifier } from '../../../../types/component';
import { MetricKey, MetricType } from '../../../../types/metrics';
import {
  ApplicationAnalysisEventCategory,
  GraphType,
  ProjectAnalysisEventCategory,
} from '../../../../types/project-activity';
import ProjectActivityAppContainer from '../ProjectActivityApp';

jest.mock('../../../../api/projectActivity');
jest.mock('../../../../api/time-machine');

jest.mock('../../../../helpers/storage', () => ({
  ...jest.requireActual('../../../../helpers/storage'),
  get: jest.fn(),
  save: jest.fn(),
}));

const projectActivityHandler = new ProjectActivityServiceMock();
const timeMachineHandler = new TimeMachineServiceMock();

beforeEach(() => {
  jest.clearAllMocks();
  projectActivityHandler.reset();
  timeMachineHandler.reset();
  timeMachineHandler.setMeasureHistory(
    [
      MetricKey.bugs,
      MetricKey.reliability_rating,
      MetricKey.code_smells,
      MetricKey.sqale_rating,
      MetricKey.security_hotspots_reviewed,
      MetricKey.security_review_rating,
    ].map((metric) =>
      mockMeasureHistory({
        metric,
        history: projectActivityHandler
          .getAnalysesList()
          .map(({ date }) => mockHistoryItem({ value: '3', date: parseDate(date) })),
      })
    )
  );
});

describe('rendering', () => {
  it('should render issues as default graph', async () => {
    const { ui } = getPageObject();
    renderProjectActivityAppContainer();
    await ui.appLoaded();

    expect(ui.graphTypeIssues.get()).toBeInTheDocument();
    expect(ui.graphs.getAll().length).toBe(1);
  });

  it('should correctly show the baseline marker', async () => {
    const { ui } = getPageObject();
    renderProjectActivityAppContainer(
      mockComponent({
        leakPeriodDate: parseDate('2017-03-01T22:00:00.000Z').toDateString(),
        breadcrumbs: [
          { key: 'breadcrumb', name: 'breadcrumb', qualifier: ComponentQualifier.Project },
        ],
      })
    );
    await ui.appLoaded();

    expect(ui.baseline.get()).toBeInTheDocument();
  });

  it('should only show certain security hotspot-related metrics for a project', async () => {
    const { ui } = getPageObject();
    renderProjectActivityAppContainer(
      mockComponent({
        breadcrumbs: [
          { key: 'breadcrumb', name: 'breadcrumb', qualifier: ComponentQualifier.Project },
        ],
      })
    );

    await ui.changeGraphType(GraphType.custom);
    await ui.openMetricsDropdown();
    expect(ui.metricCheckbox(MetricKey.security_hotspots_reviewed).get()).toBeInTheDocument();
    expect(ui.metricCheckbox(MetricKey.security_review_rating).query()).not.toBeInTheDocument();
  });

  it.each([ComponentQualifier.Portfolio, ComponentQualifier.SubPortfolio])(
    'should only show certain security hotspot-related metrics for a %s',
    async (qualifier) => {
      const { ui } = getPageObject();
      renderProjectActivityAppContainer(
        mockComponent({
          qualifier,
          breadcrumbs: [{ key: 'breadcrumb', name: 'breadcrumb', qualifier }],
        })
      );

      await ui.changeGraphType(GraphType.custom);
      await ui.openMetricsDropdown();
      expect(ui.metricCheckbox(MetricKey.security_review_rating).get()).toBeInTheDocument();
      expect(
        ui.metricCheckbox(MetricKey.security_hotspots_reviewed).query()
      ).not.toBeInTheDocument();
    }
  );
});

describe('CRUD', () => {
  it('should correctly create, update, and delete "VERSION" events', async () => {
    const { ui } = getPageObject();
    const initialValue = '1.1-SNAPSHOT';
    const updatedValue = '1.1--SNAPSHOT';
    renderProjectActivityAppContainer(
      mockComponent({
        breadcrumbs: [
          { key: 'breadcrumb', name: 'breadcrumb', qualifier: ComponentQualifier.Project },
        ],
        configuration: { showHistory: true },
      })
    );
    await ui.appLoaded();

    await ui.addVersionEvent('1.1.0.1', initialValue);
    expect(screen.getAllByText(initialValue)[0]).toBeInTheDocument();

    await act(async () => {
      await ui.updateEvent(1, updatedValue);
      expect(screen.getAllByText(updatedValue)[0]).toBeInTheDocument();
    });

    await ui.deleteEvent(0);
    expect(screen.queryByText(updatedValue)).not.toBeInTheDocument();
  });

  it('should correctly create, update, and delete "OTHER" events', async () => {
    const { ui } = getPageObject();
    const initialValue = 'Custom event name';
    const updatedValue = 'Custom event updated name';
    renderProjectActivityAppContainer(
      mockComponent({
        breadcrumbs: [
          { key: 'breadcrumb', name: 'breadcrumb', qualifier: ComponentQualifier.Project },
        ],
        configuration: { showHistory: true },
      })
    );
    await ui.appLoaded();

    await act(async () => {
      await ui.addCustomEvent('1.1.0.1', initialValue);
      expect(screen.getByText(initialValue)).toBeInTheDocument();
    });

    await act(async () => {
      await ui.updateEvent(1, updatedValue);
      expect(screen.getByText(updatedValue)).toBeInTheDocument();
    });

    await ui.deleteEvent(0);
    expect(screen.queryByText(updatedValue)).not.toBeInTheDocument();
  });

  it('should correctly allow deletion of specific analyses', async () => {
    const { ui } = getPageObject();
    renderProjectActivityAppContainer(
      mockComponent({
        breadcrumbs: [
          { key: 'breadcrumb', name: 'breadcrumb', qualifier: ComponentQualifier.Project },
        ],
        configuration: { showHistory: true },
      })
    );
    await ui.appLoaded();

    // Most recent analysis is not deletable.
    await ui.openCogMenu('1.1.0.2');
    expect(ui.deleteAnalysisBtn.query()).not.toBeInTheDocument();

    await ui.deleteAnalysis('1.1.0.1');
    expect(screen.queryByText('1.1.0.1')).not.toBeInTheDocument();
  });
});

describe('data loading', () => {
  function getMock(namespace: string) {
    // eslint-disable-next-line jest/no-conditional-in-test
    return namespace.includes('.custom') ? 'bugs,code_smells' : GraphType.custom;
  }

  it('should load all analyses', async () => {
    const count = 1000;
    projectActivityHandler.setAnalysesList(
      times(count, (i) => {
        return mockAnalysis({
          key: `analysis-${i}`,
          date: '2016-01-01T00:00:00+0200',
        });
      })
    );
    const { ui } = getPageObject();
    renderProjectActivityAppContainer();
    await ui.appLoaded();

    expect(ui.activityItem.getAll().length).toBe(count);
  });

  it('should reload custom graph from local storage', async () => {
    jest.mocked(get).mockImplementationOnce(getMock).mockImplementationOnce(getMock);
    const { ui } = getPageObject();
    renderProjectActivityAppContainer();
    await ui.appLoaded();

    expect(ui.graphTypeCustom.get()).toBeInTheDocument();
  });

  it('should correctly fetch the top level component when dealing with sub portfolios', async () => {
    const { ui } = getPageObject();
    renderProjectActivityAppContainer(
      mockComponent({
        key: 'unknown',
        qualifier: ComponentQualifier.SubPortfolio,
        breadcrumbs: [
          { key: 'foo', name: 'foo', qualifier: ComponentQualifier.Portfolio },
          { key: 'unknown', name: 'unknown', qualifier: ComponentQualifier.SubPortfolio },
        ],
      })
    );
    await ui.appLoaded();

    // If it didn't fail, it means we correctly queried for project "foo".
    expect(ui.activityItem.getAll().length).toBe(4);
  });
});

describe('filtering', () => {
  it('should correctly filter by event category', async () => {
    projectActivityHandler.setAnalysesList([
      mockAnalysis({
        key: `analysis-1`,
        events: [],
      }),
      mockAnalysis({
        key: `analysis-2`,
        events: [
          mockAnalysisEvent({ key: '1', category: ProjectAnalysisEventCategory.QualityGate }),
        ],
      }),
      mockAnalysis({
        key: `analysis-3`,
        events: [mockAnalysisEvent({ key: '2', category: ProjectAnalysisEventCategory.Version })],
      }),
      mockAnalysis({
        key: `analysis-4`,
        events: [mockAnalysisEvent({ key: '3', category: ProjectAnalysisEventCategory.Version })],
      }),
    ]);

    const { ui } = getPageObject();
    renderProjectActivityAppContainer();
    await ui.appLoaded();

    await ui.filterByCategory(ProjectAnalysisEventCategory.Version);
    expect(ui.activityItem.getAll().length).toBe(2);

    await ui.filterByCategory(ProjectAnalysisEventCategory.QualityGate);
    expect(ui.activityItem.getAll().length).toBe(1);
  });

  it('should correctly filter by date range', async () => {
    projectActivityHandler.setAnalysesList(
      times(20, (i) => {
        const date = parseDate('2016-01-01T00:00:00.000Z');
        date.setDate(date.getDate() + i);
        return mockAnalysis({
          key: `analysis-${i}`,
          date: date.toDateString(),
        });
      })
    );
    const { ui } = getPageObject();
    renderProjectActivityAppContainer();
    await ui.appLoaded();

    expect(ui.activityItem.getAll().length).toBe(20);

    await ui.setDateRange('2016-01-10');
    expect(ui.activityItem.getAll().length).toBe(11);
    await ui.resetDateFilters();

    expect(ui.activityItem.getAll().length).toBe(20);

    await ui.setDateRange('2016-01-10', '2016-01-11');
    expect(ui.activityItem.getAll().length).toBe(2);
    await ui.resetDateFilters();

    await ui.setDateRange(undefined, '2016-01-08');
    expect(ui.activityItem.getAll().length).toBe(8);
  });
});

describe('graph interactions', () => {
  it('should allow analyses to be clicked to see details for the analysis', async () => {
    const { ui } = getPageObject();
    renderProjectActivityAppContainer();
    await ui.appLoaded();

    expect(ui.bugsPopupCell.query()).not.toBeInTheDocument();
    await act(async () => {
      await ui.showDetails('1.1.0.1');
    });
    expect(ui.bugsPopupCell.get()).toBeInTheDocument();
  });

  it('should correctly handle customizing the graph', async () => {
    const { ui } = getPageObject();
    renderProjectActivityAppContainer();
    await ui.appLoaded();

    await ui.changeGraphType(GraphType.custom);

    expect(ui.noDataText.get()).toBeInTheDocument();

    // Add metrics.
    await ui.openMetricsDropdown();
    await ui.toggleMetric(MetricKey.bugs);
    await ui.toggleMetric(MetricKey.security_hotspots_reviewed);
    await ui.closeMetricsDropdown();

    expect(ui.graphs.getAll()).toHaveLength(2);

    // Remove metrics.
    await ui.openMetricsDropdown();
    await ui.toggleMetric(MetricKey.bugs);
    await ui.toggleMetric(MetricKey.security_hotspots_reviewed);
    await ui.closeMetricsDropdown();

    expect(ui.noDataText.get()).toBeInTheDocument();

    await ui.changeGraphType(GraphType.issues);

    expect(ui.graphs.getAll()).toHaveLength(1);
  });
});

function getPageObject() {
  const user = userEvent.setup();
  const ui = {
    // Graph types.
    graphTypeSelect: byLabelText('project_activity.graphs.choose_type'),
    graphTypeIssues: byText('project_activity.graphs.issues'),
    graphTypeCustom: byText('project_activity.graphs.custom'),

    // Graphs.
    graphs: byLabelText('project_activity.graphs.explanation_x', { exact: false }),
    noDataText: byText('project_activity.graphs.custom.no_history'),

    // Add metrics.
    addMetricBtn: byRole('button', { name: 'project_activity.graphs.custom.add' }),
    metricCheckbox: (name: MetricKey) => byRole('checkbox', { name }),

    // Filtering.
    categorySelect: byRole('combobox', { name: 'project_activity.filter_events' }),
    resetDatesBtn: byRole('button', { name: 'project_activity.reset_dates' }),
    fromDateInput: byRole('textbox', { name: 'start_date' }),
    toDateInput: byRole('textbox', { name: 'end_date' }),

    // Analysis interactions.
    activityItem: byLabelText(/project_activity.show_analysis_X_on_graph/),
    cogBtn: (id: string) => byRole('button', { name: `project_activity.analysis_X_actions.${id}` }),
    seeDetailsBtn: (time: string) =>
      byRole('button', { name: `project_activity.show_analysis_X_on_graph.${time}` }),
    addCustomEventBtn: byRole('button', { name: 'project_activity.add_custom_event' }),
    addVersionEvenBtn: byRole('button', { name: 'project_activity.add_version' }),
    deleteAnalysisBtn: byRole('button', { name: 'project_activity.delete_analysis' }),
    editEventBtn: byRole('button', { name: 'project_activity.events.tooltip.edit' }),
    deleteEventBtn: byRole('button', { name: 'project_activity.events.tooltip.delete' }),

    // Event modal.
    nameInput: byLabelText('name'),
    saveBtn: byRole('button', { name: 'save' }),
    changeBtn: byRole('button', { name: 'change_verb' }),
    deleteBtn: byRole('button', { name: 'delete' }),

    // Misc.
    loading: byLabelText('loading'),
    baseline: byText('project_activity.new_code_period_start'),
    bugsPopupCell: byRole('cell', { name: 'bugs' }),
  };

  return {
    user,
    ui: {
      ...ui,
      async appLoaded() {
        await waitFor(() => {
          expect(ui.loading.query()).not.toBeInTheDocument();
        });
      },
      async changeGraphType(type: GraphType) {
        await selectEvent.select(ui.graphTypeSelect.get(), [`project_activity.graphs.${type}`]);
      },
      async openMetricsDropdown() {
        await user.click(ui.addMetricBtn.get());
      },
      async toggleMetric(metric: MetricKey) {
        await user.click(ui.metricCheckbox(metric).get());
      },
      async closeMetricsDropdown() {
        await user.keyboard('{Escape}');
      },
      async openCogMenu(id: string) {
        await user.click(ui.cogBtn(id).get());
      },
      async deleteAnalysis(id: string) {
        await user.click(ui.cogBtn(id).get());
        await user.click(ui.deleteAnalysisBtn.get());
        await user.click(ui.deleteBtn.get());
      },
      async addVersionEvent(id: string, value: string) {
        await user.click(ui.cogBtn(id).get());
        await user.click(ui.addVersionEvenBtn.get());
        await user.type(ui.nameInput.get(), value);
        await user.click(ui.saveBtn.get());
      },
      async addCustomEvent(id: string, value: string) {
        await user.click(ui.cogBtn(id).get());
        await user.click(ui.addCustomEventBtn.get());
        await user.type(ui.nameInput.get(), value);
        await user.click(ui.saveBtn.get());
      },
      async updateEvent(index: number, value: string) {
        await user.click(ui.editEventBtn.getAll()[index]);
        await user.clear(ui.nameInput.get());
        await user.type(ui.nameInput.get(), value);
        await user.click(ui.changeBtn.get());
      },
      async deleteEvent(index: number) {
        await user.click(ui.deleteEventBtn.getAll()[index]);
        await user.click(ui.deleteBtn.get());
      },
      async showDetails(id: string) {
        await user.click(ui.seeDetailsBtn(id).get());
      },
      async filterByCategory(
        category: ProjectAnalysisEventCategory | ApplicationAnalysisEventCategory
      ) {
        await selectEvent.select(ui.categorySelect.get(), [`event.category.${category}`]);
      },
      async setDateRange(from?: string, to?: string) {
        const dateInput = dateInputEvent(user);
        if (from) {
          await dateInput.pickDate(ui.fromDateInput.get(), parseDate(from));
        }
        if (to) {
          await dateInput.pickDate(ui.toDateInput.get(), parseDate(to));
        }
      },
      async resetDateFilters() {
        await user.click(ui.resetDatesBtn.get());
      },
    },
  };
}

function renderProjectActivityAppContainer(
  component = mockComponent({
    breadcrumbs: [{ key: 'breadcrumb', name: 'breadcrumb', qualifier: ComponentQualifier.Project }],
  })
) {
  return renderAppWithComponentContext(
    'project/activity',
    () => <Route path="*" element={<ProjectActivityAppContainer />} />,
    {
      metrics: keyBy(
        [
          mockMetric({ key: MetricKey.bugs, type: MetricType.Integer }),
          mockMetric({ key: MetricKey.code_smells, type: MetricType.Integer }),
          mockMetric({ key: MetricKey.security_hotspots_reviewed }),
          mockMetric({ key: MetricKey.security_review_rating, type: MetricType.Rating }),
        ],
        'key'
      ),
    },
    { component }
  );
}
